﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using Gemli.Reflection;

namespace Gemli.Data.Providers
{
    /// <summary>
    /// When implemented, provides basic CRUD services for 
    /// <see cref="DataModel"/> objects.
    /// </summary>
    public abstract class DataProviderBase
    {
        /// <summary>
        /// When implemented, loads the first <see cref="DataModel"/> that
        /// the specified <paramref name="query"/> finds,
        /// within the specified database <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        public abstract TModel LoadModel<TModel>(DataModelQuery<TModel> query, DbTransaction transactionContext) 
            where TModel : DataModel;

        /// <summary>
        /// When implemented, loads the first <see cref="DataModel"/> that
        /// the specified <paramref name="query"/> finds.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public TModel LoadModel<TModel>(DataModelQuery<TModel> query) where TModel : DataModel
        {
            return LoadModel(query, null);
        }

        private static string __LoadModelMethodName;
        private DataModel LoadModel(DataModelQuery query, DbTransaction transactionContext)
        {
            if (query.GetType().IsGenericType)
            {
                MethodInfo[] mis = GetType().GetMethods();
                foreach (MethodInfo mi in mis)
                {
                    if (mi.Name ==
                        (__LoadModelMethodName ?? 
                        (__LoadModelMethodName = MethodInfo.GetCurrentMethod().Name)) && 
                        mi.IsGenericMethod)
                    {
                        MethodInfo xmi = mi.MakeGenericMethod(query.GetType().GetGenericArguments()[0]);
                        return (DataModel) xmi.Invoke(this, new object[] {query, transactionContext});
                    }
                }
            }
            throw new ArgumentException("DataModelQuery must be generic and typed.", "query");
        }

        /// <summary>
        /// When implemented, loads a set of <see cref="DataModel"/> objects
        /// using the specified <paramref name="query"/> within the
        /// specified database <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        public abstract DataModelCollection<TModel> LoadModels<TModel>(
            DataModelQuery<TModel> query, DbTransaction transactionContext)
            where TModel : DataModel;

        /// <summary>
        /// When implemented, loads a set of <see cref="DataModel"/> objects
        /// using the specified <paramref name="query"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public DataModelCollection<TModel> LoadModels<TModel>(
            DataModelQuery<TModel> query) where TModel : DataModel
        {
            return LoadModels(query, null);
        }

        private IDataModelCollection LoadModels(DataModelQuery query)
        {
            return LoadModels(query, null);
        }

        private static string __LoadModelsMethodName;
        private IDataModelCollection LoadModels(DataModelQuery query,
                                                   DbTransaction transactionContext)
        {
            if (query.GetType().IsGenericType)
            {
                MethodInfo[] mis = GetType().GetMethods();
                foreach (MethodInfo mi in mis)
                {
                    if (mi.Name ==
                        (__LoadModelsMethodName ?? (__LoadModelsMethodName = MethodInfo.GetCurrentMethod().Name)) &&
                        mi.IsGenericMethod)
                    {
                        MethodInfo xmi = mi.MakeGenericMethod(query.GetType().GetGenericArguments()[0]);
                        return (IDataModelCollection) xmi.Invoke(this, new object[] {query, transactionContext});
                    }
                }
            }
            throw new ArgumentException("DataModelQuery must be a generic type.", "query");
        }

        /// <summary>
        /// When implemented, saves the changes that were made to the
        /// specified <paramref name="dataModel"/>, within the specified
        /// database <paramref name="transactionContext"/>. 
        /// <remarks>
        /// The specified object must have the Deleted property set to true,
        /// IsDirty evaluated as true, or IsNew evaluated as true, or else
        /// the model will not be saved.
        /// </remarks>
        /// </summary>
        /// <param name="dataModel"></param>
        /// <param name="transactionContext"></param>
        public abstract void SaveModel(DataModel dataModel, DbTransaction transactionContext);

        /// <summary>
        /// When implemented, saves the changes that were made to the
        /// specified <paramref name="dataModel"/>. 
        /// <remarks>
        /// The specified object must have the Deleted property set to true,
        /// IsDirty evaluated as true, or IsNew evaluated as true, or else
        /// the model will not be saved.
        /// </remarks>
        /// </summary>
        /// <param name="dataModel"></param>
        public void SaveModel(DataModel dataModel)
        {
            SaveModel(dataModel, null);
            dataModel.DataProvider = this;
        }

        /// <summary>
        /// When implemented, saves the changes that were made to each of the
        /// specified <paramref name="dataModels"/>, within the specified
        /// database <paramref name="transactionContext"/>. 
        /// <remarks>
        /// The specified <paramref name="dataModels"/> must have the Deleted 
        /// property set to true, IsDirty evaluated as true, or IsNew 
        /// evaluated as true, or else the model will not be saved.
        /// </remarks>
        /// </summary>
        /// <param name="dataModels"></param>
        /// <param name="transactionContext"></param>
        public abstract void SaveModels<TModel>(DataModelCollection<TModel> dataModels,
                                             DbTransaction transactionContext) where TModel : DataModel;

        /// <summary>
        /// When implemented, saves the changes that were made to each of the
        /// specified <paramref name="dataModels"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="dataModels"></param>
        public void SaveModels<TModel>(DataModelCollection<TModel> dataModels) where TModel : DataModel
        {
            SaveModels(dataModels, null);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public TModel DeepLoadModel<TModel>(DataModelQuery<TModel> query) where TModel : DataModel
        {
            return DeepLoadModel(query, (int?)null);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public TModel DeepLoadModel<TModel>(DataModelQuery<TModel> query, int? depth) where TModel : DataModel
        {
            return DeepLoadModel(query, depth, null);
        }


        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        public TModel DeepLoadModel<TModel>(
            DataModelQuery<TModel> query, DbTransaction transactionContext)
            where TModel : DataModel
        {
            return DeepLoadModel(query, null, transactionContext);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public TModel DeepLoadModel<TModel>(
            DataModelQuery<TModel> query, int? depth, DbTransaction transactionContext)
            where TModel : DataModel
        {
            DataModel ret = DeepLoadModel((DataModelQuery)query, depth, transactionContext);
            if (ret is TModel) return (TModel)ret;
            if (ret.Entity is TModel) return (TModel)ret.Entity;
            Type t = typeof(DataModel<>).MakeGenericType(ret.GetType());
            ret = (DataModel)Activator.CreateInstance(t, ret);
            return (TModel)ret;
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        private DataModel DeepLoadModel(
            DataModelQuery query, DbTransaction transactionContext)
        {
            return DeepLoadModel(query, null, transactionContext);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        private DataModel DeepLoadModel(
            DataModelQuery query, int? depth, DbTransaction transactionContext)
        {
            var lst = new List<DataModel>();
            return DeepLoadModel(query, depth, transactionContext, lst);
        }

        private void LoadMember(
            DataModel dataModel,
            DataModelColumnAttribute fieldMapping,
            string memberName,
            DbTransaction transactionContext,
            List<DataModel> loadedModels)
        {
            LoadMember(dataModel, null, fieldMapping, memberName,
                       transactionContext, loadedModels);
        }

        private void LoadMember(
            DataModel dataModel,
            int? depth,
            DataModelColumnAttribute fieldMapping,
            string memberName,
            DbTransaction transactionContext,
            List<DataModel> loadedModels)
        {
            DataModelColumnAttribute field = fieldMapping;
            DataModel e = dataModel;
            ForeignKeyAttribute fkmapping = field.ForeignKeyMapping;
            string fMemberName = field.ForeignKeyMapping.AssignToMember;
            FieldInfo memField = e.Entity.GetType().GetField(fMemberName);
            PropertyInfo memProp = e.Entity.GetType().GetProperty(fMemberName);
            Type memberType = memField != null
                                  ? memField.FieldType
                                  : memProp.PropertyType;
            bool useAssignWrapper = false;
            if (!memberType.IsDataModel())
            {
                useAssignWrapper = true;
            }
            Type dataMemberType;
            dataMemberType = useAssignWrapper 
                ? typeof (DataModel<>).MakeGenericType(memberType) 
                : memberType;
            Type queryType = typeof (DataModelQuery<>).MakeGenericType(dataMemberType);
            DataModelQuery subquery = ((DataModelQuery) Activator.CreateInstance(queryType))
                .WhereProperty[fkmapping.ForeignEntityProperty].IsEqualTo(e[memberName]);
            if (depth != null) depth = depth - 1;
            DataModel subentity = DeepLoadModel(subquery, depth, transactionContext, loadedModels);
            if (useAssignWrapper)
                memProp.SetValue(
                    e.Entity, subentity.Entity, new object[] {});
            else memProp.SetValue(e, subentity, new object[] {});
        }


        private TModel DeepLoadModel<TModel>(
            DataModelQuery<TModel> query, DbTransaction transactionContext,
            List<DataModel> loadedModels
            ) where TModel : DataModel
        {
            return DeepLoadModel(query, null, transactionContext, loadedModels);
        }

        private TModel DeepLoadModel<TModel>(
            DataModelQuery<TModel> query, int? depth, DbTransaction transactionContext,
            List<DataModel> loadedModels
            ) where TModel : DataModel
        {
            DataModel ret = DeepLoadModel((DataModelQuery)query, depth, transactionContext, loadedModels);
            if (ret is TModel) return (TModel) ret;
            if (ret.Entity is TModel) return (TModel) ret;
            Type t = typeof (DataModel<>).MakeGenericType(ret.GetType());
            ret = (DataModel) Activator.CreateInstance(t, ret);
            return (TModel) ret;
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="loadedModels">
        /// Used for keeping recursive loading from resulting
        /// in infinite loops. Evaluate each loaded item from 
        /// a database result set against this collection; if 
        /// there is a match, use the collection item, 
        /// otherwise use the database loaded item, deep-load
        /// it, and add it to this collection.
        /// </param>
        /// <returns></returns>
        protected virtual DataModel DeepLoadModel(
            DataModelQuery query, DbTransaction transactionContext,
            List<DataModel> loadedModels
            )
        {
            return DeepLoadModel(query, null, transactionContext, loadedModels);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the first
        /// <see cref="DataModel"/> that is returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="depth"></param>
        /// <param name="loadedModels">
        /// Used for keeping recursive loading from resulting
        /// in infinite loops. Evaluate each loaded item from 
        /// a database result set against this collection; if 
        /// there is a match, use the collection item, 
        /// otherwise use the database loaded item, deep-load
        /// it, and add it to this collection.
        /// </param>
        /// <returns></returns>
        protected virtual DataModel DeepLoadModel(
            DataModelQuery query, int? depth, DbTransaction transactionContext,
            List<DataModel> loadedModels
            )
        {
            DataModel e = LoadModel(query, transactionContext);
            if (loadedModels.Contains(e)) return loadedModels[loadedModels.IndexOf(e)];
            foreach (DataModel previouslyLoadedModel in loadedModels)
            {
                if (query.GetType().IsGenericType)
                {
                    Type qt = query.GetType().GetGenericArguments()[0];
                    if (previouslyLoadedModel.GetType().IsOrInherits(qt) &&
                        previouslyLoadedModel.Equals(e))
                        return previouslyLoadedModel;
                }
            }
            if (!loadedModels.Contains(e)) loadedModels.Add(e);
            foreach (var fe_kvp in e.EntityMappings.ForeignModelMappings)
            {
                if (depth == null || depth > 0)
                {
                    ForeignDataModelAttribute fe = fe_kvp.Value;
                    Type targetEntityType = fe.TargetMemberType;
                    while (targetEntityType.IsGenericType &&
                           targetEntityType.IsOrInherits(typeof (IEnumerable)))
                    {
                        targetEntityType = targetEntityType.GetGenericArguments().Last();
                    }
                    if (!targetEntityType.IsDataModel())
                        targetEntityType = typeof (DataModel<>).MakeGenericType(targetEntityType);
                    Type subQueryType = typeof (DataModelQuery<>).MakeGenericType(targetEntityType);

                    Relationship relationship = fe.Relationship;
                    if (relationship == Relationship.ManyToMany &&
                        string.IsNullOrEmpty(fe.MappingTable))
                    {
                        relationship = Relationship.OneToMany;
                    }
                    switch (relationship)
                    {
                        case Relationship.OneToOne:
                        case Relationship.ManyToOne:
                            var subQuery = ((DataModelQuery) Activator.CreateInstance(subQueryType))
                                .WhereColumn[fe.RelatedTableColumn].IsEqualTo(
                                e.ColumnMappedValue[fe.RelatedTableColumn]);
                            DataModel e2 = (depth == null)
                                                ? DeepLoadModel(subQuery, transactionContext, loadedModels)
                                                : DeepLoadModel(subQuery, depth - 1, transactionContext, loadedModels);
                            object e2o = e2;
                            if (!fe.TargetMemberType.IsDataModel())
                            {
                                e2o = (e2).Entity;
                            }
                            if (fe.TargetMember.MemberType == MemberTypes.Field)
                            {
                                ((FieldInfo) fe.TargetMember).SetValue(e.Entity, e2o);
                            }
                            else if (fe.TargetMember.MemberType == MemberTypes.Property)
                            {
                                ((PropertyInfo) fe.TargetMember).SetValue(e.Entity, e2o, new object[] {});
                            }
                            break;
                        case Relationship.OneToMany:
                            var subQuery2 = ((DataModelQuery) Activator.CreateInstance(subQueryType))
                                .WhereColumn[fe.RelatedTableColumn].IsEqualTo(
                                e.ColumnMappedValue[fe.RelatedTableColumn]);
                            IDataModelCollection e2c = (depth == null)
                                                            ? DeepLoadModels(subQuery2, transactionContext,
                                                                               loadedModels)
                                                            : DeepLoadModels(subQuery2, depth - 1, transactionContext,
                                                                               loadedModels);
                            object e2ct = Activator.CreateInstance(fe.TargetMemberType);
                            if (e2ct is IList)
                            {
                                bool de = fe.TargetMemberType.IsGenericType &&
                                          fe.TargetMemberType.GetGenericArguments().Last().IsDataModel();
                                foreach (object e2cx in e2c)
                                {
                                    if (de)
                                    {
                                        ((IList) e2ct).Add(e2cx);
                                    }
                                    else
                                    {
                                        object e2cx2 = ((DataModel) e2cx).Entity;
                                        ((IList) e2ct).Add(e2cx2);
                                    }
                                }
                            }
                            else e2ct = ((IList) e2c)[0];
                            if (fe.TargetMember.MemberType == MemberTypes.Field)
                            {
                                ((FieldInfo) fe.TargetMember).SetValue(e.Entity, e2ct);
                            }
                            else if (fe.TargetMember.MemberType == MemberTypes.Property)
                            {
                                ((PropertyInfo) fe.TargetMember).SetValue(e.Entity, e2ct, new object[] {});
                            }
                            break;
                        case Relationship.ManyToMany:
                            if (!fe.TargetMemberType.IsOrInherits(typeof (IList)))
                                throw new InvalidCastException(
                                    "Cannot apply ManyToMany binding to a non-IList property.");

                            Type tright = (fe.TargetMemberType.IsGenericType &&
                                           !fe.TargetMemberType.IsDataModel() &&
                                           fe.TargetMemberType.IsOrInherits(typeof (IList)))
                                              ? fe.TargetMemberType.GetGenericArguments()[0]
                                              :
                                                  fe.TargetMemberType;
                            if (!tright.IsDataModel())
                            {
                                tright = typeof (DataModel<>).MakeGenericType(tright);
                            }
                            Type mapType = typeof (DataModelMap.RuntimeMappingTable<,>)
                                .MakeGenericType(
                                // left side of mapping table
                                fe.TargetMember.DeclaringType,
                                // right side
                                tright);
                            var mapObj = (DataModel) Activator.CreateInstance(mapType);
                            mapObj.EntityMappings.TableMapping.Schema = fe.MappingTableSchema;
                            mapObj.EntityMappings.TableMapping.Table = fe.MappingTable;
                            var mapLeftCol = mapObj.EntityMappings.ColumnMappings["LeftColumn"];
                            mapLeftCol.ColumnName = fe.LocalColumn;
                            mapLeftCol.DbType = e.EntityMappings
                                .GetFieldMappingByDbColumnName(fe.LocalColumn).DbType;
                            mapLeftCol.TargetMemberType = e.EntityMappings
                                .GetFieldMappingByDbColumnName(fe.LocalColumn).TargetMemberType;
                            ForeignDataModelAttribute mapForeignEntity = mapObj.EntityMappings
                                .ForeignModelMappings["ForeignModelCollection"];
                            mapForeignEntity.LocalColumn = fe.LocalColumn;
                            mapForeignEntity.RelatedTableSchema = fe.RelatedTableSchema;
                            mapForeignEntity.RelatedTable = fe.RelatedTable;
                            mapForeignEntity.RelatedTableColumn = fe.RelatedTableColumn;
                            mapForeignEntity.TargetMemberType = fe.TargetMemberType;

                            mapObj["LeftColumn"] = e.ColumnMappedValue[fe.LocalColumn];

                            Type mapQueryType = typeof (DataModelQuery<>).MakeGenericType(new[] {mapType});
                            DataModelQuery mapQuery = (DataModelQuery) Activator.CreateInstance(mapQueryType);
                            mapQuery.WhereColumn[mapForeignEntity.LocalColumn].IsEqualTo(
                                e.ColumnMappedValue[fe.LocalColumn]);
                            var mapdes = LoadModels(mapQuery, transactionContext);

                            var mappedDEs = new DataModelCollection<DataModel>();
                            foreach (DataModel de in mapdes) // de is a MappingTable<L,R>
                            {
                                var mappedDEQuery = (DataModelQuery)
                                                    Activator.CreateInstance(typeof (DataModelQuery<>)
                                                                                 .MakeGenericType(targetEntityType));
                                mappedDEQuery.WhereColumn[fe.RelatedTableColumn]
                                    .IsEqualTo(de.ColumnMappedValue[fe.RelatedTableColumn]); //de.LeftColumn);
                                var mappedDE = (depth == null)
                                                   ? DeepLoadModel(mappedDEQuery, transactionContext, loadedModels)
                                                   : DeepLoadModel(mappedDEQuery, depth - 1, transactionContext,
                                                                    loadedModels);
                                mappedDEs.Add(mappedDE);
                            }

                            Type mmtargtype = fe.TargetMemberType;
                            var mmtargcol = (IList) Activator.CreateInstance(fe.TargetMemberType);
                            Type mapdeType = null;
                            foreach (DataModel mapde in mappedDEs)
                            {
                                if (mapdeType == null) mapdeType = mapde.GetType();
                                object deinst = mapde;
                                if (mmtargtype.IsGenericType &&
                                    !mmtargtype.GetGenericArguments()[0].IsDataModel())
                                {
                                    deinst = mapde.Entity;
                                }
                                mmtargcol.Add(deinst);
                            }
                            if (fe.TargetMember is FieldInfo)
                                ((FieldInfo) fe.TargetMember).SetValue(e, mmtargcol);
                            else if (fe.TargetMember is PropertyInfo)
                                ((PropertyInfo) fe.TargetMember).SetValue(
                                    e.Entity, mmtargcol, new object[] {});
                            break;
                    }
                }
            }
            foreach (var field_kvp in e.EntityMappings.ColumnMappings)
            {
                DataModelColumnAttribute field = field_kvp.Value;
                if (field.IsForeignKey &&
                    field.ForeignKeyMapping.AssignToMember != null)
                {
                    LoadMember(e, field, field_kvp.Key, transactionContext, loadedModels);
                }
            }
            return e;
        }

        /// <summary>
        /// When implemented, loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <returns></returns>
        public DataModelCollection<TModel> DeepLoadModels<TModel>(DataModelQuery<TModel> query) where TModel : DataModel
        {
            return DeepLoadModels(query, (int?)null, null);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public DataModelCollection<TModel> DeepLoadModels<TModel>(DataModelQuery<TModel> query, int? depth) where TModel : DataModel
        {
            return DeepLoadModels(query, depth, null);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        public DataModelCollection<TModel> DeepLoadModels<TModel>(
            DataModelQuery<TModel> query, DbTransaction transactionContext)
            where TModel : DataModel
        {
            return DeepLoadModels(query, null, transactionContext);
        }

        /// <summary>
        /// When implemented, loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="query"></param>
        /// <param name="depth"></param>
        /// <param name="transactionContext"></param>
        /// <returns></returns>
        public DataModelCollection<TModel> DeepLoadModels<TModel>(
            DataModelQuery<TModel> query, int? depth, DbTransaction transactionContext)
            where TModel : DataModel
        {
            var lst = new List<DataModel>();
            return DeepLoadModels(query, depth, transactionContext, lst);
        }

        private DataModelCollection<TModel> DeepLoadModels<TModel>(
            DataModelQuery<TModel> query, DbTransaction transactionContext,
            List<DataModel> loadedModels
            ) where TModel : DataModel
        {
            return DeepLoadModels(query, null, transactionContext, loadedModels);
        }

        private DataModelCollection<TModel> DeepLoadModels<TModel>(
            DataModelQuery<TModel> query, int? depth, DbTransaction transactionContext,
            List<DataModel> loadedModels
            ) where TModel : DataModel
        {
            IDataModelCollection col = DeepLoadModels((DataModelQuery)query, depth, 
                transactionContext, loadedModels);
            var ret = new DataModelCollection<TModel>(col);
            return ret;
        }

        /// <summary>
        /// Loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="loadedModels">
        /// Used for keeping recursive loading from resulting
        /// in infinite loops. Evaluate each loaded item from 
        /// a database result set against this collection; if 
        /// there is a match, use the collection item, 
        /// otherwise use the database loaded item, deep-load
        /// it, and add it to this collection.
        /// </param>
        /// <returns></returns>
        protected virtual IDataModelCollection DeepLoadModels(
            DataModelQuery query, DbTransaction transactionContext,
            List<DataModel> loadedModels)
        {
            return DeepLoadModels(query, null, transactionContext, loadedModels);
        }

        /// <summary>
        /// Loads the full object graph for the 
        /// <see cref="DataModel"/> objects that are returned from the specified
        /// <paramref name="query"/>, within the specified database
        /// <paramref name="transactionContext"/>.
        /// </summary>
        /// <param name="query"></param>
        /// <param name="transactionContext"></param>
        /// <param name="depth"></param>
        /// <param name="loadedModels">
        /// Used for keeping recursive loading from resulting
        /// in infinite loops. Evaluate each loaded item from 
        /// a database result set against this collection; if 
        /// there is a match, use the collection item, 
        /// otherwise use the database loaded item, deep-load
        /// it, and add it to this collection.
        /// </param>
        /// <returns></returns>
        protected virtual IDataModelCollection DeepLoadModels(
            DataModelQuery query, int? depth, DbTransaction transactionContext,
            List<DataModel> loadedModels)
        {
            IDataModelCollection entities = LoadModels(query);
            DataModel e;
            bool hasForeignKey = false;
            if (entities.Count > 0)
            {
                e = entities.GetDataModelAt(0);
                foreach (var field_kvp in e.EntityMappings.ColumnMappings)
                {
                    DataModelColumnAttribute field = field_kvp.Value;
                    if (field.IsForeignKey)
                    {
                        hasForeignKey = true;
                        break;
                    }
                }
                if (!hasForeignKey &&
                    (e.EntityMappings.ForeignModelMappings == null ||
                    e.EntityMappings.ForeignModelMappings.Count == 0))
                {
                    return entities;
                }
            }
            for (int i = 0; i < entities.Count; i++)
            {
                if (depth == null || depth > 0)
                {
                    DataModel entity = entities.GetDataModelAt(i);
                    if (hasForeignKey && !loadedModels.Contains(entity))
                    {
                        foreach (var field_kvp in entity.EntityMappings.ColumnMappings)
                        {
                            DataModelColumnAttribute field = field_kvp.Value;
                            if (field.IsForeignKey && field.ForeignKeyMapping.AssignToMember != null)
                            {
                                LoadMember(entity, field, field_kvp.Key,
                                                 transactionContext, loadedModels);
                                loadedModels.Add(entity);
                            }
                        }
                    }
                    if (entity.EntityMappings.ForeignModelMappings != null &&
                        entity.EntityMappings.ForeignModelMappings.Count > 0
                        )
                    {
                        DataModelQuery query2 = EntityToIdentifyingQuery(entity);
                        entity = (depth == null)
                                     ? DeepLoadModel(query2, transactionContext, loadedModels)
                                     : DeepLoadModel(query2, depth - 1, transactionContext, loadedModels);
                        //loadedModels.Add(entity);
                        entities.SetDataModelAt(i, entity);
                    }
                }
            }
            return entities;
        }

        /// <summary>
        /// Returns a query that identifies the entity.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="entity"></param>
        /// <returns></returns>
        public static DataModelQuery EntityToIdentifyingQuery<TModel>(TModel entity) where TModel : DataModel
        {
            DataModelQuery query;
            if (typeof(TModel) == typeof(DataModel))
            {
                // T was down-casted; DataModel base class has no queryable metadata.
                query = (DataModelQuery) Activator.CreateInstance(
                                    typeof (DataModelQuery<>).MakeGenericType(entity.GetType()));
            }
            else
            {
                query = new DataModelQuery<TModel>();
            }
            bool pkfound = false;
            int pkc = 0;
            if (entity.EntityMappings.PrimaryKeyColumns.Length > 0)
            {
                foreach (var field_kvp in entity.EntityMappings.ColumnMappings)
                {
                    DataModelColumnAttribute field = field_kvp.Value;
                    if (field.IsPrimaryKey)
                    {
                        string memName = field_kvp.Key;
                        pkfound = (entity[memName] != null);
                        if (pkfound)
                        {
                            query.WhereProperty[memName].IsEqualTo(entity[memName]);
                            pkc++;
                        }
                    }
                }
            }
            if (!pkfound || entity.EntityMappings.PrimaryKeyColumns.Length != pkc)
            {
                foreach (var field_kvp in entity.EntityMappings.ColumnMappings)
                {
                    string memName = field_kvp.Key;
                    query.WhereProperty[memName].IsEqualTo(entity[memName]);
                }
            }
            return query;
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <paramref name="entity"/>, where any of its properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <param name="entity"></param>
        public void DeepSaveModel(DataModel entity)
        {
            DeepSaveModel(entity, null);
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <paramref name="entity"/>, within the specified
        /// database <paramref name="transactionContext"/>,
        /// where any of its properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="transactionContext"></param>
        public virtual void DeepSaveModel(DataModel entity, DbTransaction transactionContext)
        {
            var savedModels = new List<DataModel>();
            DeepSaveModel(entity, transactionContext, savedModels);
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <paramref name="entity"/>, within the specified
        /// database <paramref name="transactionContext"/>,
        /// where any of its properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <param name="entity"></param>
        /// <param name="transactionContext"></param>
        /// <param name="savedModels"></param>
        protected virtual void DeepSaveModel(DataModel entity, DbTransaction transactionContext,
            List<DataModel> savedModels)
        {
            if (savedModels.Contains(entity)) return;
            SaveModel(entity, transactionContext);
            var t = entity.GetType();
            while (t.IsGenericType && t.IsDataModel())
            {
                t = t.GetGenericArguments()[0];
            }
            var fis = t.GetFields();
            var pis = t.GetProperties();
            var mis = new List<MemberInfo>();
            foreach (var fi in fis) mis.Add(fi);
            foreach (var pi in pis) mis.Add(pi);
            foreach (var mi in mis)
            {
                var fks = mi.GetCustomAttributes(typeof (ForeignKeyAttribute), true);
                var fes = mi.GetCustomAttributes(typeof (ForeignDataModelAttribute), true);
                if (fks.Length > 0 || fes.Length > 0)
                {
                    object subentity = null;
                    try
                    {
                        if (mi is FieldInfo) subentity = ((FieldInfo) mi).GetValue(entity);
                        if (mi is PropertyInfo)
                            subentity = ((PropertyInfo) mi).GetValue(entity.Entity, new object[] {});
                    }
                    catch
                    {
                    }
                    if (subentity == null) continue;
                    if (subentity is IEnumerable && !subentity.IsDataModel())
                    {
                        foreach (object enmrsubentity in (IEnumerable) subentity)
                        {
                            object subentityitem = enmrsubentity;
                            if (!enmrsubentity.IsDataModel())
                            {
                                // We'll wrap and save anyway because even though
                                // the entity isn't a DataModel, its properties
                                // might be, and we're doing a deep save.
                                var seit = subentityitem.GetType();
                                var subentityitemT = typeof (DataModel<>).MakeGenericType(seit);
                                subentityitem = Activator.CreateInstance(subentityitemT, subentityitem);
                            }
                            SaveModel((DataModel) subentityitem, transactionContext);
                            savedModels.Add((DataModel) subentityitem);
                        }
                    }
                    else
                    {
                        if (!subentity.IsDataModel())
                        {
                            // We'll wrap and save anyway because even though
                            // the entity isn't a DataModel, its properties
                            // might be, and we're doing a deep save.
                            var seit = subentity.GetType();
                            var subentityT = typeof(DataModel<>).MakeGenericType(seit);
                            subentity = Activator.CreateInstance(subentityT, subentity);
                        }
                        SaveModel((DataModel)subentity, transactionContext);
                            savedModels.Add((DataModel) subentity);
                    }
                }

            }
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <see cref="DataModel"/> objects,
        /// where any of their properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="col"></param>
        public void DeepSaveModels<TModel>(DataModelCollection<TModel> col) where TModel : DataModel
        {
            DeepSaveModels(col, null);
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <see cref="DataModel"/> objects, within the specified
        /// database <paramref name="transactionContext"/>,
        /// where any of their properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="col"></param>
        /// <param name="transactionContext"></param>
        public void DeepSaveModels<TModel>(
            DataModelCollection<TModel> col, 
            DbTransaction transactionContext) 
            where TModel : DataModel
        {
            var savedModels = new List<DataModel>();
            DeepSaveModels(col, transactionContext, savedModels);
        }

        /// <summary>
        /// Saves the entire object graph of the specified
        /// <see cref="DataModel"/> objects, within the specified
        /// database <paramref name="transactionContext"/>,
        /// where any of their properties
        /// are also <see cref="DataModel"/> objects with
        /// IsDirty, Deleted, or IsNew change state.
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <param name="col"></param>
        /// <param name="transactionContext"></param>
        /// <param name="savedModels"></param>
        public virtual void DeepSaveModels<TModel>(
            DataModelCollection<TModel> col,
            DbTransaction transactionContext, 
            List<DataModel> savedModels)
            where TModel : DataModel
        {
            foreach (var entity in col)
            {
                if (savedModels.Contains(entity)) continue;
                entity.SynchronizeFields(SyncTo.FieldMappedData);
                DeepSaveModel(entity, transactionContext);
                savedModels.Add(entity);
            }
        }

        /// <summary>
        /// Returns true if the data provider supports the 
        /// creation and/or handling of a DbTransaction.
        /// </summary>
        public abstract bool SupportsTransactions { get; }

        /// <summary>
        /// When implemented, starts a database transaction.
        /// </summary>
        /// <returns></returns>
        public abstract DbTransaction BeginTransaction();

        /// <summary>
        /// When implemented, starts a database transaction
        /// with the specified isolation level.
        /// </summary>
        /// <returns></returns>
        public abstract DbTransaction BeginTransaction(System.Data.IsolationLevel isolationLevel);
    }
}